package sm.dam;

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.dom4j.Document;
import org.hibernate.CacheMode;
import org.hibernate.FlushMode;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.orm.hibernate3.HibernateTemplate;

import sm.dam.api.Converter;
import sm.dam.api.DBDictService;
import sm.dam.api.DDLAdaptor;
import sm.dam.api.DDLAdaptorFactory;
import sm.dbdict.DBDictChangePolicy;
import sm.dbdict.DBDictChangeUtil;
import sm.dbdict.DBDictUtil;
import sm.dbdict.change.DBDictChange;
import sm.dbdict.change.SqlColumnChange;
import sm.dbdict.change.SqlTableChange;
import sm.dbdict.def.DBDict;
import sm.dbdict.def.Field;
import sm.dbdict.def.Key;
import sm.dbdict.def.SqlFieldOptions;
import sm.dbdict.def.SqlIndex;
import sm.dbdict.def.SqlTables;
import sm.hibernate.HibernateMappingUtil;
import sm.util.SMMap;
import sm.util.Util;

public class DBDictServiceImpl implements DBDictService {

	private File hibernateConfig;

	private File mappingDir;

	private File tempMappingDir;

	private HibernateTemplate template;

	private DBDictChangePolicy dbdictChangePolicy;

	public DBDictChangePolicy getDbdictChangePolicy() {
		return dbdictChangePolicy;
	}

	public void setDbdictChangePolicy(DBDictChangePolicy dbdictChangePolicy) {
		this.dbdictChangePolicy = dbdictChangePolicy;
	}

	public File getHibernateConfig() {
		return hibernateConfig;
	}

	public void setHibernateConfig(File hibernateConfig) {
		this.hibernateConfig = hibernateConfig;
	}

	public File getTempMappingDir() {
		return tempMappingDir;
	}

	public void setTempMappingDir(File tempMappingDir) {
		this.tempMappingDir = tempMappingDir;
	}

	public File getMappingDir() {
		return mappingDir;
	}

	public void setMappingDir(File mappingDir) {
		this.mappingDir = mappingDir;
	}

	public HibernateTemplate getTemplate() {
		return template;
	}

	public void setTemplate(HibernateTemplate template) {
		this.template = template;
	}

	private void executeDDL(final String ddl) {
		if (ddl != null) {
			template.execute(new HibernateCallback() {
				@Override
				public Object doInHibernate(Session session)
						throws HibernateException, SQLException {
					int result = session.createSQLQuery(ddl).executeUpdate();
					return result;
				}
			});
		}
	}

	/**
	 * return a table, return success or not
	 * 
	 * @param dbdict
	 * @param sqlTable
	 * @return
	 */
	public boolean executeCreateTable(DBDict dbdict, SqlTables sqlTable) {
		String tableName = sqlTable.getSql_table_name();

		// skip null table
		if (DDLAdaptor.NULLTABLE.equals(tableName)) {
			return false;
		}

		String tableAlias = sqlTable.getSql_table_alias();
		List<Field> dbFields = DBDictUtil.getDBFields(dbdict, tableAlias);
		if (dbFields.size() > 0) {
			Map<String, String> dbColumns = new LinkedHashMap<String, String>();
			for (Field dbField : dbFields) {
				SqlFieldOptions sqlFieldOptions = dbField
						.getSql_field_options();
				String columName = sqlFieldOptions.getSql_column_name();
				String columType = sqlFieldOptions.getSql_data_type();
				dbColumns.put(columName, columType);
			}

			DDLAdaptor ddlAdaptor = DDLAdaptorFactory.getDDLAdaptor(sqlTable
					.getSql_db_type());
			boolean isArrayTable = DBDictUtil.isArrayTable(dbdict, tableAlias);
			if (isArrayTable) {
				dbColumns.put(DDLAdaptor.RECORD_NUMBER,
						ddlAdaptor.getRecordNumberType());
			}
			Key pk = DBDictUtil.getPrimaryKey(dbdict);
			if (pk == null) {
				dbColumns.put(AbstractDDLAdaptor.RECORD_KEY,
						ddlAdaptor.getRecordKeyType());
			}
			String ddl = ddlAdaptor.generateCreateTable(tableName, dbColumns);
			executeDDL(ddl);
			return true;
		} else { // this table has no fields, ignore it
			return false;
		}
	}

	/**
	 * create an index, create RECORD_KEY if index is null, return success or
	 * not
	 * 
	 * @param dbdict
	 * @param sqlTable
	 * @param key
	 * @return
	 */
	public boolean executeCreateIndex(DBDict dbdict, SqlTables sqlTable,
			SqlIndex index, List<Field> keyFields) {
		String indexName = null;
		List<String> dbColumns = new ArrayList<String>();

		String tableName = sqlTable.getSql_table_name();

		if (index != null) {
			indexName = index.getIndexName();
			for (Field keyField : keyFields) {
				if (DBDictUtil.isDBColumn(keyField)) {
					dbColumns.add(keyField.getSql_field_options()
							.getSql_column_name());
				} else {
					indexName = null;
					break;
				}
			}
		} else {
			indexName = DDLAdaptorFactory.createIndexName(tableName);
			dbColumns.add(AbstractDDLAdaptor.RECORD_KEY);
		}

		if (indexName != null) {
			String tableAlias = sqlTable.getSql_table_alias();
			boolean isArrayTable = DBDictUtil.isArrayTable(dbdict, tableAlias);
			if (isArrayTable) {
				dbColumns.add(DDLAdaptor.RECORD_NUMBER);
			}
			boolean isUnique = false;
			if (index == null || index.getType() == 1) {
				isUnique = true;
			}

			DDLAdaptor ddlAdaptor = DDLAdaptorFactory.getDDLAdaptor(sqlTable
					.getSql_db_type());
			String ddl = ddlAdaptor.generateCreateIndex(indexName, isUnique,
					tableName, dbColumns);
			executeDDL(ddl);
			return true;
		} else {
			return false;
		}
	}

	private void executeCreateTableWithIndex(DBDict dbdict, SqlTables sqlTable) {
		if (executeCreateTable(dbdict, sqlTable)) {
			String tableName = sqlTable.getSql_table_name();
			Key pk = DBDictUtil.getPrimaryKey(dbdict);
			if (pk == null) { // create RECORD_KEY
				executeCreateIndex(dbdict, sqlTable, null,
						Collections.EMPTY_LIST);
			}

			for (Key key : dbdict.getKey()) {
				SqlIndex index = DBDictUtil.getSqlIndexes(key, tableName);
				if (index != null) {
					List<Field> keyFields = DBDictUtil.getKeyFields(key,
							dbdict.getField());
					executeCreateIndex(dbdict, sqlTable, index, keyFields);
				}
			}
		}
	}

	private void executeRenameTableWithIndex(DBDict tempDBDict,
			SqlTables tempTable, Map<String, String> tableNamesMapping,
			Map<String, String> indexNamesMapping) {
		String tempTableName = tempTable.getSql_table_name();
		String newTableName = tableNamesMapping.get(tempTableName);

		if (newTableName != null) {
			DDLAdaptor ddlAdaptor = DDLAdaptorFactory.getDDLAdaptor(tempTable
					.getSql_db_type());
			final String renameTableDDL = ddlAdaptor.generateRenameTable(
					tempTableName, newTableName);
			executeDDL(renameTableDDL);

			for (Key tempKey : tempDBDict.getKey()) {
				SqlIndex sqlIndex = DBDictUtil.getSqlIndexes(tempKey,
						tempTableName);
				if (sqlIndex != null) {
					String tempIndexName = sqlIndex.getIndexName();
					String newIndexName = indexNamesMapping.get(tempIndexName);
					if (newIndexName != null) {
						final String renameIndexDDL = ddlAdaptor
								.generateRenameIndex(tempIndexName,
										newIndexName);
						executeDDL(renameIndexDDL);
					}
				}
			}
		}
	}

	private void executeAlterChange(DBDictChange change, DBDict newDBDict) {
		for (SqlTables newTable : change.getNewTables()) {
			executeCreateTableWithIndex(newDBDict, newTable);
		}

		for (Entry<String, SqlTableChange> tableChange : change
				.getTableChanges().entrySet()) {
			String tableName = tableChange.getKey();
			SqlTables table = DBDictUtil.getTableByName(
					newDBDict.getSql_tables(), tableName);
			DDLAdaptor ddlAdaptor = DDLAdaptorFactory.getDDLAdaptor(table
					.getSql_db_type());

			SqlTableChange sqlTableChange = tableChange.getValue();
			for (SqlColumnChange columnChange : sqlTableChange.getNewColumns()) {
				final String alterColumnChange = ddlAdaptor.generateAddColumn(
						tableName, columnChange.getColumnName(),
						columnChange.getDataType());
				template.execute(new HibernateCallback() {
					@Override
					public Object doInHibernate(Session session)
							throws HibernateException, SQLException {
						int result = session.createSQLQuery(alterColumnChange)
								.executeUpdate();
						return result;
					}
				});
			}
			for (SqlColumnChange columnChange : sqlTableChange
					.getColumnChanges()) {
				final String alterColumnChange = ddlAdaptor
						.generateModifyColumn(tableName,
								columnChange.getColumnName(),
								columnChange.getDataType());
				template.execute(new HibernateCallback() {
					@Override
					public Object doInHibernate(Session session)
							throws HibernateException, SQLException {
						int result = session.createSQLQuery(alterColumnChange)
								.executeUpdate();
						return result;
					}
				});
			}
		}
	}

	private void fullTableCopy(DBDict oldDBDict, DBDict newDBDict)
			throws Exception {
		// create temp table
		String fileName = newDBDict.getName();

		Map<String, String> tableNamesMapping = new HashMap<String, String>();
		Map<String, String> indexNamesMapping = new HashMap<String, String>();
		// create temp dbdict
		DBDict tempDBDict = (DBDict) newDBDict.clone();
		for (Key tempKey : tempDBDict.getKey()) {
			List<SqlIndex> indexes = tempKey.getSqlIndexes();
			if (indexes != null) {
				for (SqlIndex index : indexes) {
					String tableName = index.getTableName();
					String tempTableName = DDLAdaptorFactory
							.createTempEntityName(tableName);
					index.setTableName(tempTableName);
					tableNamesMapping.put(tempTableName, tableName);

					String indexName = index.getIndexName();
					String tempIndexName = DDLAdaptorFactory
							.createTempEntityName(indexName);
					index.setIndexName(tempIndexName);
					indexNamesMapping.put(tempIndexName, indexName);
				}
			}
		}
		for (SqlTables tempTable : tempDBDict.getSql_tables()) {
			String tableName = tempTable.getSql_table_name();
			String tempTableName = DDLAdaptorFactory
					.createTempEntityName(tableName);
			tempTable.setSql_table_name(tempTableName);
			if (!tableNamesMapping.containsKey(tempTableName)) {
				tableNamesMapping.put(tempTableName, tableName);
			}
		}

		// create temp table and index
		for (SqlTables tempTable : tempDBDict.getSql_tables()) {
			if (executeCreateTable(tempDBDict, tempTable)) {
				String tempTableName = tempTable.getSql_table_name();
				Key pk = DBDictUtil.getPrimaryKey(tempDBDict);
				if (pk == null) { // create RECORD_KEY
					executeCreateIndex(tempDBDict, tempTable, null,
							Collections.EMPTY_LIST);
				}

				for (Key tempKey : tempDBDict.getKey()) {
					SqlIndex tempIndex = DBDictUtil.getSqlIndexes(tempKey,
							tempTableName);
					if (tempIndex != null) {
						List<Field> tempKeyFields = DBDictUtil.getKeyFields(
								tempKey, tempDBDict.getField());
						if (!executeCreateIndex(tempDBDict, tempTable,
								tempIndex, tempKeyFields)) {
							indexNamesMapping.remove(tempIndex.getIndexName());
						}
					}
				}
			} else {
				tableNamesMapping.remove(tempTable.getSql_table_name());
			}
		}

		// create temp hibernate
		Document tempHBM = HibernateMappingUtil.generateHBM(tempDBDict);
		File tempHBMFile = HibernateMappingUtil.saveHBM(fileName,
				tempMappingDir, tempHBM);
		Configuration tempConf = new Configuration().configure(hibernateConfig)
				.addFile(tempHBMFile);
		SessionFactory sf = tempConf.buildSessionFactory();
		Session session = sf.openSession();
		session.setCacheMode(CacheMode.IGNORE);
		session.setFlushMode(FlushMode.COMMIT);
		Transaction tr = session.beginTransaction();
		String entityName = Util.radEntityNameToJava(fileName);
		// move data
		try {
			List<Object> allDatas = template.find("from " + entityName);
			for (Object data : allDatas) {
				SMMap dataMap = (SMMap) data;

				// clone it for temp table
				SMMap tempDataMap = (SMMap) dataMap.clone();

				// convert data
				if (dbdictChangePolicy != null) {
					Map<String, Converter> converters = dbdictChangePolicy
							.getConverters(fileName);
					if (converters != null) {
						for (Entry<String, Converter> entry : converters
								.entrySet()) {
							String fieldPath = entry.getKey();
							Converter converter = entry.getValue();
							Object oldValue = tempDataMap.get(fieldPath);
							Object newValue = converter.convert(oldValue,
									tempDataMap);
							tempDataMap.put(fieldPath, newValue);
						}
					}
				}

				// update descriptor
				tempDataMap.put(DBDictService.DESCRIPTOR, tempDataMap.clone());
				session.save(entityName, tempDataMap);
			}
			tr.commit();
		} catch (Exception e) {
			tr.rollback();
			// drop temp tables
			for (SqlTables tempTable : tempDBDict.getSql_tables()) {
				DDLAdaptor ddlAdaptor = DDLAdaptorFactory
						.getDDLAdaptor(tempTable.getSql_db_type());
				final String dropTableDDL = ddlAdaptor
						.generateDropTable(tempTable.getSql_table_name());
				executeDDL(dropTableDDL);
			}
			throw new DataIntegrityViolationException(e.getMessage(), e);
		} finally {
			session.close();
			sf.close();
		}

		// drop old tables
		for (SqlTables oldTable : oldDBDict.getSql_tables()) {
			DDLAdaptor ddlAdaptor = DDLAdaptorFactory.getDDLAdaptor(oldTable
					.getSql_db_type());
			final String dropTableDDL = ddlAdaptor.generateDropTable(oldTable
					.getSql_table_name());
			executeDDL(dropTableDDL);
		}

		// rename temp tables
		for (SqlTables tempTable : tempDBDict.getSql_tables()) {
			executeRenameTableWithIndex(tempDBDict, tempTable,
					tableNamesMapping, indexNamesMapping);
		}
	}

	@Override
	public Serializable saveDBDict(DBDict dbdict) {
		DBDictUtil.refineDBDict(dbdict);
		String fileName = dbdict.getName();
		for (SqlTables sqlTable : dbdict.getSql_tables()) {
			executeCreateTableWithIndex(dbdict, sqlTable);
		}

		if (!DBDictService.DBDICT.equals(fileName)) {
			try {
				Document hbm = HibernateMappingUtil.generateHBM(dbdict);
				HibernateMappingUtil.saveHBM(fileName, mappingDir, hbm);
			} catch (IOException e) {
				throw new DataIntegrityViolationException(e.getMessage(), e);
			}
		}

		SMMap dbdictMap = new SMMap();
		dbdictMap.put(NAME, fileName);
		dbdictMap.put(DESCRIPTOR, dbdict);
		return template.save(DBDICT, dbdictMap);
	}

	@Override
	public void updateDBDict(DBDict newDBDict) {
		DBDictUtil.refineDBDict(newDBDict);
		String fileName = newDBDict.getName();
		SMMap dbdictMap = (SMMap) template.get(DBDICT, fileName);
		DBDict oldDBDict = (DBDict) dbdictMap.get(DESCRIPTOR);
		try {
			DBDictChange change = new DBDictChange();
			if (DBDictChangeUtil.checkDBDictChange(oldDBDict, newDBDict,
					change, dbdictChangePolicy)) {
				// trigger full table copy
				fullTableCopy(oldDBDict, newDBDict);
			} else {
				executeAlterChange(change, newDBDict);
			}
		} catch (Exception e) {
			throw new DataIntegrityViolationException(e.getMessage(), e);
		}

		if (!DBDictService.DBDICT.equals(fileName)) {
			try {
				Document hbm = HibernateMappingUtil.generateHBM(newDBDict);
				HibernateMappingUtil.saveHBM(fileName, mappingDir, hbm);
			} catch (IOException e) {
				throw new DataIntegrityViolationException(e.getMessage(), e);
			}
		}

		dbdictMap.put(DESCRIPTOR, newDBDict);
		template.update(DBDICT, dbdictMap);
	}

	@Override
	public DBDict getDBDict(String fileName) {
		DBDict des = null;
		Object dbdict = template.get(DBDictService.DBDICT, fileName);
		if (dbdict != null) {
			SMMap dbdictMap = (SMMap) dbdict;
			des = (DBDict) dbdictMap.get(DBDictService.DESCRIPTOR);
		}
		return des;
	}
}
